﻿#region Header

/*
Behavioral Rating of Dancing Human Crowds based on Motion Patterns
By

Pascal Hauser 
Dipl. Ing. in Informatik, Hochschule für Technik Rapperswil, 2006
Master Thesis, Hochschule für Technik Rapperswil, 2008-2010

and

Raphael Gfeller
Dipl. Ing. in Informatik, Hochschule für Technik Rapperswil, 2006
Master Thesis, Hochschule für Technik Rapperswil, 2008-2010

*/

#endregion

#region Usings

using System;
using System.Collections.Generic;
using paravili.Services;
using Sebarf.Diagnostics.Interfaces;
using Sebarf.Services.Interfaces;

#endregion

namespace paravili.Steps {
	/// <summary>
	/// tries to find movement between the given list of points
	/// </summary>
	public class FindMovementBetweenClustersByUsingService :
		ProcessStepConverterWithMeasurement<IList<IList<ColoredPoint>>, IList<Cluster>> {
		#region Public Properties

		[ConfigurableNumericValue(Name = "MaxDistanceBetweenTwoCluster", RangeFrom = 10, RangeTo = 500)]
		public int MaxDistanceBetweenTwoCluster { get; set; }

		[ConfigurableNumericValue(Name = "MaxFramesBetweenTwoCluster", RangeFrom = 0, RangeTo = 50)]
		public int MaxFramesBetweenTwoCluster { get; set; }

		[ConfigurableNumericValue(Name = "WeightOfDistance", RangeFrom = 1, RangeTo = 10)]
		public int WeightOfDistance { get; set; }

		[ConfigurableNumericValue(Name = "WeightOfTime", RangeFrom = 1, RangeTo = 10)]
		public int WeightOfTime { get; set; }

		[ConfigurableNumericValue(Name = "WeightOfColour", RangeFrom = 1, RangeTo = 10)]
		public int WeightOfColour { get; set; }

		[ConfigurableNumericValue(Name = "WeightConvexHullSize", RangeFrom = 1, RangeTo = 10)]
		public int WeightConvexHullSize { get; set; }

		[ConfigurabelBooleanValue(Name = "use a robust method based on time")]
		public bool DetectRobustMovementOverTime { get; set; }

		[ConfigurableNumericValue(Name = "how many previous clusters are analyzed for a robust movement detection", RangeFrom = 1, RangeTo = 10)]
		public int NumberOfFramesUsedForRobustnessCalculation { get; set; }

		[ServiceRequest]
		public ITimeService TimeService { get; set; }

		[ServiceRequest]
		public IResetHandlerService ResetHandlerService {
			set { value.Reset += OnReset; }
		}

		[ServiceRequest]
		public IDoMatchingService DoMatchingService { set; get; }

		#endregion

		#region Public Methods

		public FindMovementBetweenClustersByUsingService() {
			MaxDistanceBetweenTwoCluster = 60;
			MaxFramesBetweenTwoCluster = 4;
			WeightOfDistance = 8;
			WeightOfTime = 1;
			WeightOfColour = 1;
			WeightConvexHullSize = 1;
			NumberOfFramesUsedForRobustnessCalculation = 5;
			DetectRobustMovementOverTime = true;
			DoReset();
		}

		#endregion

		#region Private Methods

		protected override IList<Cluster> Convert(IList<IList<ColoredPoint>> toProcess) {
			if (toProcess != null) {
				// prepare data
				foreach (var pointList in toProcess) {
					m_availableClusters.Add(new Cluster(pointList, TimeService.GetTime()));
				}
			}
			IList<Cluster> toReturn = FindMovement();
			return toReturn;
		}

		/// <summary>
		/// Reset handler
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="args"></param>
		private void OnReset(Object sender, EventArgs args) {
			DoReset();
		}

		/// <summary>
		/// resets the step
		/// </summary>
		private void DoReset() {
			m_availableClusters.Clear();
			m_minDistance = double.MaxValue;
			m_minColor = double.MaxValue;
			m_minSize = double.MaxValue;
			m_minTime = double.MaxValue;
			m_maxColor = double.MinValue;
			m_maxDistance = double.MinValue;
			m_maxSize = double.MinValue;
			m_maxTime = double.MinValue;
		}

		private double MatchFunction(Cluster first, Cluster last) {
			return MatchFunction(first, last, false);
		}

		/// <summary>
		/// Calculates how close two given clusters are
		/// </summary>
		/// <param name="newerCluster"></param>
		/// <param name="olderCluster"></param>
		/// <returns></returns>
		private double MatchFunction(Cluster newerCluster, Cluster olderCluster, bool ignoreTimeAndDistance) {
			if (newerCluster == olderCluster) {
				return double.MinValue;
			}
			if (newerCluster.PreviousCluster != null) {
				return double.MinValue;
			}
			double matchingPoints = 0;
			if (!ignoreTimeAndDistance) {
				double distance =
					Math.Pow(
						Math.Pow(newerCluster.AverageXValue - olderCluster.AverageXValue, 2) + Math.Pow(newerCluster.AverageYValue - olderCluster.AverageYValue, 2),
						0.5);
				if (MaxDistanceBetweenTwoCluster < distance) {
					return double.MinValue;
				}
				m_minDistance = Math.Min(m_minDistance, distance);
				m_maxDistance = Math.Max(m_maxDistance, distance);

				matchingPoints -= Normalize(distance, m_minDistance, m_maxDistance, WeightOfDistance);

				// (distance / ReturnOneIfNull(m_maxDistance - m_minDistance) * WeightOfDistance);

				int timeDiff = Math.Abs(newerCluster.Time - olderCluster.Time);

				if (timeDiff == 0) {
					return double.MinValue;
				}
				if (newerCluster.Time < olderCluster.Time || timeDiff > (MaxFramesBetweenTwoCluster + 1)) {
					return double.MinValue;
				}
				if (timeDiff != 1) {
					m_minTime = Math.Min(m_minTime, timeDiff);
					m_maxTime = Math.Max(m_maxTime, timeDiff);
					//matchingPoints -= Normalize(timeDiff, m_minTime, m_maxTime, WeightOfTime);
				}
			}
			double colorDifferenceDifference =
				Math.Pow(
					Math.Pow(newerCluster.ColorY - olderCluster.ColorY, 2) + Math.Pow(newerCluster.ColorX - olderCluster.ColorX, 2) +
					Math.Pow(newerCluster.ColorZ - olderCluster.ColorZ, 2), 0.5);

			m_minColor = Math.Min(m_minColor, colorDifferenceDifference);
			m_maxColor = Math.Max(m_maxColor, colorDifferenceDifference);
			matchingPoints -= Normalize(colorDifferenceDifference, m_minColor, m_maxColor, WeightOfColour);

			double diffConvexHullSize = Math.Abs(newerCluster.HulPerimeterSize - olderCluster.HulPerimeterSize);

			m_minSize = Math.Min(m_minSize, diffConvexHullSize);
			m_maxSize = Math.Max(m_maxSize, diffConvexHullSize);

			matchingPoints -= Normalize(diffConvexHullSize, m_minSize, m_maxSize, WeightConvexHullSize);

			return matchingPoints;
		}

		/// <summary>
		/// helper function to avoid null values
		/// </summary>
		/// <param name="d"></param>
		/// <returns></returns>
		private static double ReturnOneIfNull(double d) {
			//if (d == 0) {
			//		return 1;
			//	}
			return d;
		}

		private static double Normalize(double value, double minValue, double maxValue, double weight) {
			if (value == minValue && minValue == maxValue) {
				return 0;
			}
			double ratio = 100.0 / Math.Abs(ReturnOneIfNull(maxValue - minValue)) * (value - ReturnOneIfNull(minValue));
			if (ratio == 0) {
				return 0;
			}
			return Math.Pow(Math.E, ratio / 10.0 * weight);
		}

		/// <summary>
		/// match clusters together
		/// </summary>
		/// <returns></returns>
		private IList<Cluster> FindMovement() {
			if (m_availableClusters.Count < 1) {
				return null;
			}
			// remove to old clusters
			int maxTime = MaxFramesBetweenTwoCluster;
			int time = TimeService.GetTime();
			m_availableClusters.RemoveAll(c => time - c.Time > maxTime);

			// rember found clusters
			IList<Cluster> toReturn = new List<Cluster>();
			IList<Cluster> toRemove = new List<Cluster>();
			Cluster firstCluster;
			Cluster lastCluster;
			var distanceBetweenClusters = new int[m_availableClusters.Count, m_availableClusters.Count];
			Cluster[] clusters = m_availableClusters.ToArray();

			double[] preCalculatedDivisors = new double[NumberOfFramesUsedForRobustnessCalculation];
			if (DetectRobustMovementOverTime) {
				preCalculatedDivisors[0] = 1;
				for (int i = 1; i < NumberOfFramesUsedForRobustnessCalculation; i++) {
					preCalculatedDivisors[i] = preCalculatedDivisors[i - 1] + 1.0 / (i * i);
				}

			}
			// prepare data
			for (int i = 0; i < clusters.Length; i++) {
				for (int j = 0; j < clusters.Length; j++) {
					var distance = 0.0;
					if (DetectRobustMovementOverTime) {
						int index = 1;
						foreach (var c in clusters[j]) {

							// calc distance and weight factor
							distance += (MatchFunction(clusters[i], c, index > 1)) * 1000 * 1.0 / (index * index);
							index++;
							if (double.IsNegativeInfinity(distance) || distance == double.MinValue) {
								break;
							}
							// 

							if (!(index <= NumberOfFramesUsedForRobustnessCalculation)) {
								break;
							}
						}
						if (index > 2) {

							// normalize
							distance /= preCalculatedDivisors[index - 2];
						}
					}
					else {
						distance = (MatchFunction(clusters[i], clusters[j]) * 1000);
					}
					distanceBetweenClusters[i, j] = (int)distance;
				}
			}

			//do match
			Match[] matches = DoMatchingService.FindMatch(distanceBetweenClusters);

			// uses matches
			foreach (Match match in matches) {
				int firstClusterIndex = match.ToIndex;
				int lastClusterIndex = match.FromIndex;
				lastCluster = clusters[lastClusterIndex];
				firstCluster = clusters[firstClusterIndex];
				lastCluster.PreviousCluster = firstCluster;
				toReturn.Add(lastCluster);
				toRemove.Add(firstCluster);
			}
			foreach (Cluster cluster in toRemove) {
				m_availableClusters.Remove(cluster);
			}
			StatisticValuesProviderService.SetStatisticValue("Movements.Found", toReturn.Count);
			StatisticValuesProviderService.SetStatisticValue("AvailableClusters.Left", m_availableClusters.Count);
			return toReturn;
		}

		#endregion

		#region Private Fields

		private readonly List<Cluster> m_availableClusters = new List<Cluster>();
		private double m_maxColor;
		private double m_maxDistance;
		private double m_maxSize;
		private double m_maxTime;
		private double m_minColor;
		private double m_minDistance;
		private double m_minSize;
		private double m_minTime;

		#endregion
	}
}